__init__()
方法的程式碼@dataclass()
的參數field()
的使用asdict()
將 data class 轉型成 dict前幾日都在講一些幫助專案開發的工具、技巧與規範,今天開始便會與本次的挑戰主題 —「基於自然語言處理的新聞意見提取應用」關聯更加密切。
這次使用的繁體中文新聞資料主要來自台灣的一些網路新聞網站。從一篇新聞網頁上擷取下來的資料包含了不同的類型,比如說:新聞的發入日期(表現方式多變)、新聞中的照片內容(圖片連結與描述對)、新聞的數段內文(數個 html 中的 <p><\p>
),要在 Python 程式中的函數中傳遞這些資料,將資料包成 class 會讓之後寫程式輕鬆許多。
一般多數 Python 教程中都包含了 class 的基本用法,今天要介紹的 Data Class 用法比起一般的 class ,我認為能更容易的用來表示一篇網路新聞的內容,之後要儲存到資料庫也會更加便利。
dataclass 是屬於 Python 3.9 標準函式庫(Standard Library)中的模組,最早描述在 PEP 557 – Data Classes 當中,並且有著如下描述:
可以把 Data Classes 想成是 「具有預設值的可改變 nametuple(mutable namedtuples with defaults,nametuple 是另一種 python 用法)」 ,因為 Data Classes(dataclass 用法)使用普通的 class 定義語法,所以可以配合 inheritance 、 metaclasses 、 docstrings、自定義 methods、class factories以及其他 Python class 用法一同使用。
Data Class 會搭配 class decorator(裝飾器,@dataclass
)使用,會檢查 class 定義中具有類型註釋(type annotation)的變數,如同 PEP 526 – Syntax for Variable Annotations 中所描述。
由於在還不知道 dataclass 得用法下,很難解釋其優點,所以在介紹 dataclass 的用法時,邊聊我認為 dataclass 的優點。
本段程式碼範例引用自 dataclasses — Data Classes,並參考文檔內容
下面是一個搭配 dataclass 定義庫存 class 的範例程式:
from dataclasses import dataclass
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
上面程式碼中的名稱與類型對(如:
name: str
)稱之為 field
__init__()
方法的程式碼仔細一看,有沒有發現上面的 class 缺少了一般都會有的 __init__
,原因是使用 @dataclass
裝飾器,會自動根據 fields的資訊,在這裡是 name: str
、unit_price: float
、quantity_on_hand: int = 0
(這裡表示quantity_on_hand 預設為 0) 這幾個自定義的變數名稱與類型(type)對,建立相應的 __init__()
方法,並將其加入 class等同在這個範例中少寫下面部分:
def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
self.name = name
self.unit_price = unit_price
self.quantity_on_hand = quantity_on_hand
優點(ㄧ)
讓修改 instance attribute (實例屬性)方便超多!
假設要為 InventoryItem 這個 class 新增一個 instance attribute,類型為 int 且名稱為 weight。
一般情況下需要更改def __init__(self, weight:int, 其餘照舊)
,並新增self.weight = weight
。用了@dataclass
後只需要在對的位置新增短短的一行weight: int
,減少了所需的步驟。
@dataclass()
的參數下面三種 @dataclass
的用法是等價的:
@dataclass
class C:
...
@dataclass()
class C:
...
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
...
其中第三種括號中的內容顯示了 @dataclass
參數的預設值,這些參數代表的意義如下:
init:
__init__()
方法。__init__()
,則忽略此參數。repr:
__repr__()
方法,生成的 repr 字串將具有 class 名稱以及每個 instance attribute 名稱和 repr ,並按照在 instance attribute 在class 中的定義的順序。InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)
優點(二)
用 print() 印出 instance 時,repr 字串中會包含了 instance attribute 的值,這對於 debug 與很有幫助,如下所示:
>>> class A:
... def __init__(name: str="YOU"):
... self.name = name
>>> a = A()
>>> print(a)
<__main__.A object at 0x104b9bd00>
...
>>> from dataclasses import dataclass
>>> @dataclass
... class B:
... name: str = "ME"
>>> b = B()
>>> print(b)
B(name='ME')
eq:
__eq__()
方法,只能用來比較完全相同類別(type)的 instance。此方法將 class 作為其字串的元组(tuple),按順序比較。order:
__lt__()
、 __le__()
、 __gt__()
和 __ge__()
方法。 此方法將 class 作為其字串的元组(tuple)比較,但只能用來比較完全相同類別(type)的 instance。unsafe_hash:
eq
和 frozen
的設定,產生 hash() 方法,由內建的 hash()
實作。frozen:
__setattr__()
or __delattr__()
將引發 TypeError。優點(三)
將 frozen 設定為 True 可以避免 instance attribute 的值不小心遭到改動,有助於維持資料的正確性。
試圖改動 instance attribute 將造成dataclasses.FrozenInstanceError
,如下面的說明範例:
>>> from dataclasses import dataclass
>>> @dataclass(frozen=True)
... class D:
... name:str = "XXX"
...
>>> d = D()
>>> print(d)
D(name='XXX')
>>> d.name = "OOO"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'name'
field()
的使用像是原先範例中其中一個 field quantity_on_hand: int = 0
這種使用方法固然沒有問題,但有時候會需要用到 field()
這個函數。
下面的程式碼片段中,有使用到兩個重要的設定
@dataclass
class C:
mylist: list[int] = dataclasses.field(default_factory=list, repr=False)
value: int=7
repr=False
。mylist: list[int] = []
,所以用 field(default_factory=list
。其他細節請查看 dataclasses — Data Classes
asdict()
將 data class 轉型成 dict優點(四)
使用asdict()
讓我們可以很輕鬆的將 data class 轉型成 dict。
像是網頁 API 常使用到的 json 格式,很多時候作法會需要用到將 Python dict 轉換成 json。如果使用一般的 class 就會需要自己寫 function 將 class 裡的資訊轉成 dict,相比之下asdict()
就方便的多。
>>> from dataclasses import dataclass
>>> @dataclass
>>> class InventoryItem:
... name: str
... unit_price: float
... quantity_on_hand: int = 0
...
>>> item = InventoryItem(name="ITEM", unit_price=1.25)
>>> print(item)
InventoryItem(name='ITEM', unit_price=1.25, quantity_on_hand=0)
>>> import dataclasses
>>> dataclasses.asdict(item)
{'name': 'ITEM', 'unit_price': 1.25, 'quantity_on_hand': 0}
綜合上述四個使用 Data Class 的優點,我認為配合 Data Class 來定義用來儲存新聞資料的 Class,在撰寫程式碼與後續更動及維護上會更加流暢省力。下一段將分享我如何配合 Data Class 定義用來儲存新聞資料的 Class。
「基於自然語言處理的新聞意見提取應用」中的新聞指的是繁體中文的網路政治類別新聞,預計會搜集下列媒體的政治新聞:
為了從新聞網站上獲得新聞資料,打算使用 Python 撰寫爬蟲程式。常見的 Python 爬蟲工具有:selenium、requests、beautifulsoup等。配合上述工具所寫出的程式碼,會先使用 selenium、requests等工具,獲取新聞文章頁面的 html,接著使用 beautifulsoup進行解析,並提取出目標資訊。
能從網頁 html 中提取的資訊會因新聞媒體而不同。透過觀察上一段列出的新聞媒體的文章網頁,我整理想提取的資訊,如下結構所示:
<p><\p>
)@dataclass(frozen=True)
class NewsData:
"""The class to hold the news data.
"""
time: datetime # 文章發佈時間
source: str # 新聞媒體名稱
category: str # 新聞文章類別
author: str # 記者資訊
title: str # 新聞文章標題
content: list[str] # 新聞文章內文(有分數段)
url: str # 新聞文章連結
images: list[ImageData] # 新聞文章中的圖片連結與其描述(可能有多張)
hash_tags: list[HashTagData] # hash tag 與其連結(有些媒體沒有此項目)
上面結構中的「新聞文章中的圖片與其描述」、「hash tag 與其連結」有屬於各自的結構,如下所示:
@dataclass(frozen=True)
class ImageData:
"""The class to hold the image data.
"""
url: str # 圖片連結
describe: str # 圖片描述
@dataclass(frozen=True)
class HashTagData:
"""The class to hold the hash tag data.
"""
tag: str # tag 名稱
href: int # tag 連結
使用 Data Class 所定義的新聞資料 class 是不是看起來相當簡潔?設定 frozen=True
還可以避免資料不小心被改動到。對於當中的資料類型(Type)也表示的非常清楚。這樣的作法會有助於簡化之後將上面的 NewsData class 轉換成可以使用 psycopg 的格式,再傳到 PostgreSQL 私服器的複雜度,這部分之後應該會介紹到,那麼今天的內容就先到這裡嘍~
寫的有些匆忙,如果文章有錯誤,歡迎指正~